12.3 编译

编译并不仅仅是执行“go build”命令,还有一些须额外注意的内容。

如习惯使用GDB这类调试器,建议编译时添加-gcflags”-N-l”参数阻止优化和内联,否则调试时会有各种“找不到”的情况。

package main

func test(x*int) { println(*x) }

func main() { x:=0x100 test(&x) }

输出:

$go build-gcflags”-N-l-m”

./test.go:3:test x does not escape ./test.go:9:main&x does not escape

$go tool objdump-s”main.main”test

TEXT main.main(SB)test.go test.go:7 SUBQ0x100,0x8(SP) test.go:9 LEAQ 0x8(SP),BX test.go:9 MOVQ BX,0(SP) test.go:9 CALL main.test(SB) test.go:10 ADDQ$0x10,SP test.go:10 RET

$ls-lh test -rwxr-xr-x 1 1000 1000 1.1M Mar 28 2016 test

而当发布时,参数-ldfalgs”-w-s”会让链接器剔除符号表和调试信息,除能减小可执行文件大小外,还可稍稍增加反汇编的难度。

$go build-gcflags”-m” -ldflags”-w-s”

./test.go:3:can inline test ./test.go:7:can inline main ./test.go:9:inlining call to test ./test.go:3:test x does not escape ./test.go:9:main&x does not escape

$go tool objdump-s”main.main”test objdump:disassemble test:no symbol section

$ls-lh test -rwxr-xr-x 1 1000 1000 720K Mar 28 2016 test

还可借助更专业的工具,对可执行文件进行减肥。

$upx-9 test

                  Ultimate Packer for eXecutables
                     Copyright(C)1996-2013

UPX 3.91 Markus Oberhumer,Laszlo Molnar&John Reiser Sep 30th 2013

   File size       Ratio    Format    Name

737024 244876 33.22% linux/ElfAMD test

Packed 1 file.

交叉编译

所谓交叉编译(cross compile),是指在一个平台下编译出其他平台所需的可执行文件。这对于UNIX-like开发人员很重要,因为我们习惯使用Mac或其他桌面环境。

自Go实现自举后,交叉编译变得更方便。只须使用GOOS、GOARCH环境变量指定目标平台和架构就行。

$go env GOOS darwin

$go build&&file test test:Mach-O 64-bit executable x86_64

$GOOS=linux go build&&file test test:ELF 64-bit LSB executable,x86-64,version 1(SYSV),statically linked,not stripped

$GOOS=windows GOARCH=386 go build&&file test.exe test.exe:PE32 executable for MS Windows(console)Intel 80386 32-bit

建议用go install命令为目标平台预编译好标准库,避免go build每次都须完整编译。

GOOS=linux go install cmd # 生成目标平台工具链,可选

注意:交叉编译不支持CGO。

条件编译

除在代码中用runtime.GOOS进行判断外,编译器本身就支持文件级别的条件编译。虽说没有C预编译指令那么方便,但是基于文件的组织方式更便于维护。

方法一:将平台和架构信息添加到主文件名尾部。

main.go

package main

func main() { hello() }

hello_darwin.go

package main

func hello() { println(“hello,mac.“) }

hello_linux.go

package main

func hello() { println(“hello,linux.“) }

使用GOOS交叉编译,看看具体使用哪个文件。

$GOOS=darwin go build-x compile… -pack./hello_darwin.go./main.go

$GOOS=linux go build-x compile… -pack./hello_linux.go./main.go

编译器会选择对应的源码文件进行编译。在标准库里可以看到很多类似的文件名。

$ls/usr/local/go/src/runtime/sys_*

sys_arm.go sys_darwin_arm64.s sys_linux_386.s sys_linux_ppc64x.s sys_arm64.go sys_dragonfly_amd64.s sys_linux_amd64.s sys_mips64x.go
sys_darwin_386.s sys_freebsd_386.s sys_linux_arm.s sys_nacl_386.s
sys_darwin_amd64.s sys_freebsd_amd64.s sys_linux_arm64.s sys_nacl_amd64p32.s sys_darwin_arm.s sys_freebsd_arm.s sys_linux_mips64x.s sys_nacl_arm.s

文件名中除GOOS外,还可以加上GOARCH,或任选其一。

方法二:使用build编译指令。

与用文件名区分多版本类似,build编译指令告知编译器:当前源码文件只能用于指定环境。它一样可用来区分多版本,且控制指令更加丰富和灵活。

a.go

// +build windows ------ 必须有空行 package main

func hello() { println(“hello,windows.“) }

b.go

// +build linux darwin

package main

func hello() { println(“hello,unix.“) }

可添加多条build指令,表示多个AND条件。在单一指令里,空格表示OR条件,逗号表示AND,感叹号表示NOT。

// +build linux darwin // +build 386,!cgo

相当于:

(linux OR darwin)AND(386 AND(NOT cgo))

除GOOS、GOARCH外,可用条件还有编译器、版本号等。

// +build ignore // +build gccgo // +build go1.5

更多详细信息,请阅读标准库go/build文档。

方法三:使用自定义tag指令。

除预定义build指令外,也可通过命令行tags参数传递自定义指令。

main.go

package main

func main() { hello() }

debug.go

// +build!release

package main

func hello() { println(“debug version.“) }

release.go

// +build!release

package main

func hello() { println(“debug version.“) }

log.go

// +build log

package main

func init() { println(“logging…“) }

如有多个自定义条件,用空格分开。

$go build&& ./test debug version.

$go build-tags”release log” && ./test logging… release version.

预处理

简单点说,就是用go generate命令扫描源码文件,找出所有“go:generate”注释,提取其中的命令并执行。

  • 命令必须放在.go源文件中。
  • 命令必须以“//go:generate”开头(双斜线后不能有空格)。
  • 每个文件可有多条generate命令。
  • 命令支持环境变量。
  • 必须显式执行go generate命令。
  • 按文件名顺序提取命令并执行。
  • 串行执行,出错后终止后续命令的执行。

这种设计的初衷是为包开发者准备的,可用其完成一些自动处理命令。比如在发布时,清理掉一些包用户不会使用的测试代码。除此之外,还可用来完成基于模板生成代码(类似泛型功能),或将资源文件转换为源码(.resx嵌入资源)等工作。

a.go

//go:generate echo$GOPATH //go:generate ls-lh

package main

func hello() { println(“hello world!“) }

b.go

//go:generate uname-a

package main

func init() { }

支持以下命令行参数:

参数 说明 示例 ------------------+-------------------------------------+---------------- -v 显示处理的包及文件名 -x 显示准备执行的命令 -n 仅显示命令,但不执行

$go generate-n

echo/home/yuhen/go/test ls-lh uname-a

可为当前文件中的命令定义别名(仅当前文件有效),以便多次重复使用。

//go:generate-command LX ls-l //go:generate LX/var //go:generate LX/usr